/**********************************************************************

  Audacity: A Digital Audio Editor

  ImportFLAC.cpp

  Copyright 2004  Sami Liedes
  Leland Lucius

  Based on ImportPCM.cpp by Dominic Mazzoni
  Licensed under the GNU General Public License v2 or later

*//****************************************************************//**

\class FLACImportFileHandle
\brief An ImportFileHandle for FLAC data

*//****************************************************************//**

\class FLACImportPlugin
\brief An ImportPlugin for FLAC data

*//*******************************************************************/

#include <wx/defs.h>

#include "Import.h"
#include "ImportPlugin.h"
#include "ImportProgressListener.h"

#include "Tags.h"

#define FLAC_HEADER "fLaC"

#define DESC XO("FLAC files")

static const auto exts = {
    wxT("flac"),
    wxT("flc")
};

#include <wx/file.h>
#include <wx/ffile.h>

#include "FLAC++/decoder.h"

#include "WaveTrack.h"
#include "ImportUtils.h"

#ifdef USE_LIBID3TAG
extern "C" {
#include <id3tag.h>
}
#endif

/* FLACPP_API_VERSION_CURRENT is 6 for libFLAC++ from flac-1.1.3 (see <FLAC++/export.h>) */
#if !defined FLACPP_API_VERSION_CURRENT || FLACPP_API_VERSION_CURRENT < 6
#define LEGACY_FLAC
#else
#undef LEGACY_FLAC
#endif

class FLACImportFileHandle;

class MyFLACFile final : public FLAC::Decoder::File
{
public:
    MyFLACFile(FLACImportFileHandle* handle)
        : mFile(handle)
    {
        mWasError = false;
        set_metadata_ignore_all();
        set_metadata_respond(FLAC__METADATA_TYPE_VORBIS_COMMENT);
        set_metadata_respond(FLAC__METADATA_TYPE_STREAMINFO);
    }

    bool get_was_error() const
    {
        return mWasError;
    }

    ImportProgressListener* mImportProgressListener { nullptr };

private:
    friend class FLACImportFileHandle;
    FLACImportFileHandle* mFile;
    bool mWasError;
    wxArrayString mComments;
protected:
    FLAC__StreamDecoderWriteStatus write_callback(const FLAC__Frame* frame, const FLAC__int32* const buffer[]) override;
    void metadata_callback(const FLAC__StreamMetadata* metadata) override;
    void error_callback(FLAC__StreamDecoderErrorStatus status) override;
};

class FLACImportPlugin final : public ImportPlugin
{
public:
    FLACImportPlugin()
        : ImportPlugin(FileExtensions(exts.begin(), exts.end()))
    {
    }

    ~FLACImportPlugin() { }

    wxString GetPluginStringID() override { return wxT("libflac"); }
    TranslatableString GetPluginFormatDescription() override;
    std::unique_ptr<ImportFileHandle> Open(
        const FilePath& Filename, AudacityProject*)  override;
};

class FLACImportFileHandle final : public ImportFileHandleEx
{
    friend class MyFLACFile;
public:
    FLACImportFileHandle(const FilePath& name);
    ~FLACImportFileHandle();

    bool Init();

    TranslatableString GetFileDescription() override;
    ByteCount GetFileUncompressedBytes() override;
    void Import(
        ImportProgressListener& progressListener, WaveTrackFactory* trackFactory, TrackHolders& outTracks, Tags* tags,
        std::optional<LibFileFormats::AcidizerTags>& outAcidTags) override;

    wxInt32 GetStreamCount() override { return 1; }

    const TranslatableStrings& GetStreamInfo() override
    {
        static TranslatableStrings empty;
        return empty;
    }

    void SetStreamUsage(wxInt32 WXUNUSED(StreamID), bool WXUNUSED(Use)) override
    {}

private:
    sampleFormat mFormat;
    std::unique_ptr<MyFLACFile> mFile;
    wxFFile mHandle;
    unsigned long mSampleRate;
    unsigned long mNumChannels;
    unsigned long mBitsPerSample;
    FLAC__uint64 mNumSamples;
    FLAC__uint64 mSamplesDone;
    bool mStreamInfoDone;
    WaveTrack::Holder mTrack;
};

void MyFLACFile::metadata_callback(const FLAC__StreamMetadata* metadata)
{
    switch (metadata->type) {
    case FLAC__METADATA_TYPE_VORBIS_COMMENT:
        for (FLAC__uint32 i = 0; i < metadata->data.vorbis_comment.num_comments; i++) {
            mComments.push_back(UTF8CTOWX((char*)metadata->data.vorbis_comment.comments[i].entry));
        }
        break;

    case FLAC__METADATA_TYPE_STREAMINFO:
        mFile->mSampleRate=metadata->data.stream_info.sample_rate;
        mFile->mNumChannels=metadata->data.stream_info.channels;
        mFile->mBitsPerSample=metadata->data.stream_info.bits_per_sample;
        mFile->mNumSamples=metadata->data.stream_info.total_samples;

        // Widen mFormat after examining the file header
        if (mFile->mBitsPerSample <= 16) {
            mFile->mFormat=int16Sample;
        } else if (mFile->mBitsPerSample == 24) {
            mFile->mFormat=int24Sample;
        } else {
            mFile->mFormat=floatSample;
        }
        mFile->mStreamInfoDone=true;
        break;
    // handle the other types we do nothing with to avoid a warning
    case FLAC__METADATA_TYPE_PADDING:     // do nothing with padding
    case FLAC__METADATA_TYPE_APPLICATION:       // no idea what to do with this
    case FLAC__METADATA_TYPE_SEEKTABLE:   // don't need a seektable here
    case FLAC__METADATA_TYPE_CUESHEET:    // convert this to labels?
    case FLAC__METADATA_TYPE_PICTURE:           // ignore pictures
    case FLAC__METADATA_TYPE_UNDEFINED:   // do nothing with this either

    // FIXME: not declared when compiling on Ubuntu.
    //case FLAC__MAX_METADATA_TYPE: // quiet compiler warning with this line
    default:
        break;
    }
}

void MyFLACFile::error_callback(FLAC__StreamDecoderErrorStatus WXUNUSED(status))
{
    mWasError = true;

    /*
    switch (status)
    {
    case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC:
       wxPrintf(wxT("Flac Error: Lost sync\n"));
       break;
    case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH:
       wxPrintf(wxT("Flac Error: Crc mismatch\n"));
       break;
    case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER:
       wxPrintf(wxT("Flac Error: Bad Header\n"));
       break;
    default:
       wxPrintf(wxT("Flac Error: Unknown error code\n"));
       break;
    }*/
}

FLAC__StreamDecoderWriteStatus MyFLACFile::write_callback(const FLAC__Frame* frame,
                                                          const FLAC__int32* const buffer[])
{
    // Don't let C++ exceptions propagate through libflac
    return GuardedCall< FLAC__StreamDecoderWriteStatus >([&] {

        unsigned chn = 0;
        ImportUtils::ForEachChannel(*mFile->mTrack, [&](auto& channel)
        {

            if (frame->header.bits_per_sample == 8) {
                auto tmp = ArrayOf< short > { frame->header.blocksize };
                for (unsigned int s = 0; s < frame->header.blocksize; s++) {
                    tmp[s] = buffer[chn][s] << 8;
                }
                channel.AppendBuffer((samplePtr)tmp.get(),
                                     int16Sample,
                                     frame->header.blocksize, 1,
                                     int16Sample);
            } else if (frame->header.bits_per_sample <= 16) {
                channel.AppendBuffer((samplePtr)buffer[chn],
                                     int16Sample,
                                     frame->header.blocksize, 1,
                                     int16Sample);
            } else if (frame->header.bits_per_sample <= 24) {
                channel.AppendBuffer((samplePtr)buffer[chn],
                                     int24Sample,
                                     frame->header.blocksize, 1,
                                     int24Sample);
            } else {
                auto tmp = ArrayOf< float > { frame->header.blocksize };
                for (unsigned int s = 0; s < frame->header.blocksize; s++) {
                    tmp[s] = static_cast<float>(buffer[chn][s]) / static_cast<float>(1<<(frame->header.bits_per_sample-1));
                }
                channel.AppendBuffer((samplePtr)tmp.get(),
                                     floatSample,
                                     frame->header.blocksize, 1,
                                     floatSample);
            }
            ++chn;
        });

        mFile->mSamplesDone += frame->header.blocksize;

        if (mFile->mNumSamples > 0) {
            mImportProgressListener->OnImportProgress(static_cast<double>(mFile->mSamplesDone)
                                                      / static_cast<double>(mFile->mNumSamples));
        }

        if (mFile->IsCancelled() || mFile->IsStopped()) {
            return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
        }

        return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
    }, MakeSimpleGuard(FLAC__STREAM_DECODER_WRITE_STATUS_ABORT));
}

TranslatableString FLACImportPlugin::GetPluginFormatDescription()
{
    return DESC;
}

std::unique_ptr<ImportFileHandle> FLACImportPlugin::Open(
    const FilePath& filename, AudacityProject*)
{
    // First check if it really is a FLAC file

    int cnt;
    wxFile binaryFile;
    if (!binaryFile.Open(filename)) {
        return nullptr; // File not found
    }

    // FIXME: TRAP_ERR wxFILE ops in FLAC Import could fail.
    // Seek() return value is not examined, for example.
#ifdef USE_LIBID3TAG
    // Skip any ID3 tags that might be present
    id3_byte_t query[ID3_TAG_QUERYSIZE];
    cnt = binaryFile.Read(query, sizeof(query));
    cnt = id3_tag_query(query, cnt);
    binaryFile.Seek(cnt);
#endif

    char buf[5];
    cnt = binaryFile.Read(buf, 4);
    binaryFile.Close();

    if (cnt == wxInvalidOffset || strncmp(buf, FLAC_HEADER, 4) != 0) {
        // File is not a FLAC file
        return nullptr;
    }

    // Open the file for import
    auto handle = std::make_unique<FLACImportFileHandle>(filename);

    bool success = handle->Init();
    if (!success) {
        return nullptr;
    }

    // This std::move is needed to "upcast" the pointer type
    return std::move(handle);
}

static Importer::RegisteredImportPlugin registered{ "FLAC",
                                                    std::make_unique< FLACImportPlugin >()
};

FLACImportFileHandle::FLACImportFileHandle(const FilePath& name)
    :  ImportFileHandleEx(name),
    mSamplesDone(0),
    mStreamInfoDone(false)
{
    // Initialize mFormat as narrowest
    mFormat = narrowestSampleFormat;
    mFile = std::make_unique<MyFLACFile>(this);
}

bool FLACImportFileHandle::Init()
{
#ifdef LEGACY_FLAC
    bool success = mFile->set_filename(OSINPUT(mFilename));
    if (!success) {
        return false;
    }
    mFile->set_metadata_respond(FLAC__METADATA_TYPE_STREAMINFO);
    mFile->set_metadata_respond(FLAC__METADATA_TYPE_VORBIS_COMMENT);
    FLAC::Decoder::File::State state = mFile->init();
    if (state != FLAC__FILE_DECODER_OK) {
        return false;
    }
#else
    if (!mHandle.Open(GetFilename(), wxT("rb"))) {
        return false;
    }

    // Even though there is an init() method that takes a filename, use the one that
    // takes a file handle because wxWidgets can open a file with a Unicode name and
    // libflac can't (under Windows).
    //
    // Responsibility for closing the file is passed to libflac.
    // (it happens when mFile->finish() is called)
    bool result = mFile->init(mHandle.fp()) ? true : false;
    mHandle.Detach();

    if (result != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
        return false;
    }
#endif
    mFile->process_until_end_of_metadata();

#ifdef LEGACY_FLAC
    state = mFile->get_state();
    if (state != FLAC__FILE_DECODER_OK) {
        return false;
    }
#else
    // not necessary to check state, error callback will catch errors, but here's how:
    if (mFile->get_state() > FLAC__STREAM_DECODER_READ_FRAME) {
        return false;
    }
#endif

    if (!mFile->is_valid() || mFile->get_was_error()) {
        // This probably is not a FLAC file at all
        return false;
    }
    return true;
}

TranslatableString FLACImportFileHandle::GetFileDescription()
{
    return DESC;
}

auto FLACImportFileHandle::GetFileUncompressedBytes() -> ByteCount
{
    // TODO: Get Uncompressed byte count.
    return 0;
}

void FLACImportFileHandle::Import(
    ImportProgressListener& progressListener, WaveTrackFactory* trackFactory,
    TrackHolders& outTracks, Tags* tags,
    std::optional<LibFileFormats::AcidizerTags>&)
{
    BeginImport();

    outTracks.clear();

    auto cleanup = finally([&]{ mFile->mImportProgressListener = nullptr; });

    wxASSERT(mStreamInfoDone);

    mTrack = ImportUtils::NewWaveTrack(*trackFactory, mNumChannels, mFormat, mSampleRate);

    mFile->mImportProgressListener = &progressListener;

    // TODO: Vigilant Sentry: Variable res unused after assignment (error code DA1)
    //    Should check the result.
   #ifdef LEGACY_FLAC
    bool res = (mFile->process_until_end_of_file() != 0);
   #else
    bool res = (mFile->process_until_end_of_stream() != 0);
   #endif

    if (IsCancelled()) {
        progressListener.OnImportResult(ImportProgressListener::ImportResult::Cancelled);
        return;
    }

    ImportUtils::FinalizeImport(outTracks, *mTrack);

    wxString comment;
    wxString description;

    size_t cnt = mFile->mComments.size();
    if (cnt > 0) {
        tags->Clear();
        for (size_t c = 0; c < cnt; c++) {
            wxString name = mFile->mComments[c].BeforeFirst(wxT('='));
            wxString value = mFile->mComments[c].AfterFirst(wxT('='));
            wxString upper = name.Upper();
            if (upper == wxT("DATE") && !tags->HasTag(TAG_YEAR)) {
                long val;
                if (value.length() == 4 && value.ToLong(&val)) {
                    name = TAG_YEAR;
                }
            } else if (upper == wxT("COMMENT") || upper == wxT("COMMENTS")) {
                comment = value;
                continue;
            } else if (upper == wxT("DESCRIPTION")) {
                description = value;
                continue;
            }
            tags->SetTag(name, value);
        }

        if (comment.empty()) {
            comment = description;
        }
        if (!comment.empty()) {
            tags->SetTag(TAG_COMMENTS, comment);
        }
    }

    progressListener.OnImportResult(IsStopped()
                                    ? ImportProgressListener::ImportResult::Stopped
                                    : ImportProgressListener::ImportResult::Success);
}

FLACImportFileHandle::~FLACImportFileHandle()
{
    mFile->finish();
}
